﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using CI.Mesopotamia;
using CI.Organism;
using CI.Utils;
using Microsoft.Robotics.Simulation.Engine;

namespace CI.MSRoboticsEnv
{
    internal class VisualEntityManager
    {

        // current simulation entity count
        public int SimulationEntityCount { get; set; }
        /// <summary>pause for clean up per org</summary>
        private const int CleanUpPause = 1000;
        /// <summary>Max  clean up pause time for simulation</summary>
        private const int MaxCleanUpPause = 15000;
        private readonly TimeSpan _deleteEnitytWaitTimeout = new TimeSpan(0, 0, 2);
        private readonly TimeSpan _insertEntityWaitTimeout = new TimeSpan(0, 0, 10);

        /// <summary>Count of how many environment entitys are in the engine for clean up later</summary>
        private int _emptyEntityCount;

        private readonly Action<VisualEntity, ManualResetEvent> _simDeleteEntity;
        private readonly Action<IPhysicalEnvironment, ManualResetEvent> _simInsertEntity;
        private readonly SimIdleManager _simIdleManager;

        // hash table of the environment entites
        private readonly Dictionary<Guid, IPhysicalEnvironment> _entitys = new Dictionary<Guid, IPhysicalEnvironment>();

        // any ones that errors trying to create them
        private readonly Dictionary<Guid, IPhysicalEnvironment> _errorCreatingEntitys = new Dictionary<Guid, IPhysicalEnvironment>();

        /// <summary>
        /// Simuation environment
        /// </summary>
        private readonly SimEnv _simEnv;

        public VisualEntityManager(
            Action<VisualEntity, ManualResetEvent> simDeleteEntity,
            Action<IPhysicalEnvironment, ManualResetEvent> simInsertEntity,
            SimIdleManager simIdleManager)
        {
            _simDeleteEntity = simDeleteEntity;
            _simInsertEntity = simInsertEntity;
            _simIdleManager = simIdleManager;
            _simEnv = SimEnv.Instance;
        }

        /// <summary>
        /// suspend all entity's
        /// </summary>
        /// <param name="suspend"></param>
        private void SuspendAllEntity(bool suspend)
        {
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    foreach (var entity in _entitys)
                    {
                        ((VisualEntity)entity.Value.BaseObject).SuspendUpdates = suspend;
                    }
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Error acquiring lock on _entitys");
            }
        }

        public void SetEmptyEntityCount()
        {
            _emptyEntityCount = SimulationEntityCount;

            _simEnv.TraceLogger.LogDebug("Empty Entity Count :" + _emptyEntityCount);

            // test code this should never be zero but some times is, find out why
            if (_emptyEntityCount == 0)
            {
                throw new MSRoboticsEnvService.SimulationException("Empty Entity count on start up");
            }
        }

        public bool InsertEntity(IOrganism organism, IPhysicalEnvironment visualEntity)
        {
            bool result = true;

            var insertEvent = new ManualResetEvent(false);

            _simInsertEntity(visualEntity, insertEvent);

            _simEnv.TraceLogger.LogDebug(string.Format("Waiting - Inserting Organism {0}", organism.Guid));
            if (!insertEvent.WaitOne(_insertEntityWaitTimeout))
            {
                const string errorMsg = "Error waiting on INSERT sync";
                _simEnv.TraceLogger.LogWarn(errorMsg);
                _simEnv.TriggerScenarioRestart(errorMsg);
                result = false;
            }

            insertEvent.Close();

            if (!((VisualEntity)visualEntity.BaseObject).HasBeenInitialized)
            {

                _simEnv.TraceLogger.LogDebug(string.Format("HasBeenInitialized failed on {0} with GUID {1}", organism, organism.Guid));

                _errorCreatingEntitys.Add(organism.Guid, visualEntity);

                organism.TerminateAndDestroy("Unable to add to system");

                const string errorMsg = "Environment Problems restarting";

                _simEnv.TraceLogger.LogDebug(errorMsg);

                // set to restart
                _simEnv.TriggerScenarioRestart(errorMsg);

                result = false;

            }

            // save our record of adding it if everything is okay
            if (result)
                SaveEntity(visualEntity, organism);

            return result;
        }

        /// <summary>
        /// Clean up the sim environment, delete all entitys and wait until they are gone
        /// </summary>
        public void CleanUp()
        {

            _simEnv.TraceLogger.LogDebug(string.Format("Starting Cleanup of {0} entitys and bad entitys {1}", _entitys.Count, _errorCreatingEntitys.Count));
            // suspend and delete all updates

            if (_simEnv.ServiceFailureRestart)
            {
                // if we have had a failure no point trying to delete as the service will be dropped anyway
                return;
            }

            SuspendAllEntity(true);
            DeleteAllEntitys();

            _simEnv.TraceLogger.LogDebug("Sent Delete command to Simulation Engine for all Entitys");

            // wait for catch up
            _simIdleManager.WaitForIdle();

            int timeOut = MaxCleanUpPause;

            // check and make sure they are all gone
            while ((SimulationEntityCount > _emptyEntityCount) && (timeOut > 0))
            {
                _simEnv.TraceLogger.LogDebug(string.Format("Checking Serialized Entity count {0} vs Empty Entity count {1}", SimulationEntityCount, _emptyEntityCount));

                StatsLogger.Sleeper(CleanUpPause);
                timeOut -= CleanUpPause;

                // if we timed out throw a tantrum
                if (timeOut <= 0)
                {
                    ShutdownOnError();
                    break;
                }

                // wait for catch up
                _simIdleManager.WaitForIdle();

                // if too many left try purging any ones which had errors on creation
                PurgeErrorCreatingEntitys();
            }

            // now clear the _entitys list
            ClearEntitysList();

            // now clear _errorCreatingEntitys the list
            ClearErrorsEntityList();

            _simEnv.TraceLogger.LogDebug("Finished Cleanup");
        }


        private void ShutdownOnError()
        {
            _simEnv.TraceLogger.LogWarn("ShutdownOnError :" + DateTime.Now.ToLocalTime());
            program.Exit(-1);
        }

        private void PurgeErrorCreatingEntitys()
        {
            if ((SimulationEntityCount > _emptyEntityCount))
            {
                if (Monitor.TryEnter(_errorCreatingEntitys, Globals.CriticalAcquireLockTimeout))
                {
                    try
                    {
                        if (_errorCreatingEntitys.Count > 0)
                        {
                            // remove all from environment that might be errrors
                            foreach (var kvp in _errorCreatingEntitys)
                            {
                                _simEnv.TraceLogger.LogDebug(string.Format("Testing bad entity {0} , has been init {1}", kvp.Value.Organism.Guid, ((VisualEntity)_errorCreatingEntitys[kvp.Key].BaseObject).HasBeenInitialized));
                                // make sure it has been init
                                if (!((VisualEntity)_errorCreatingEntitys[kvp.Key].BaseObject).HasBeenInitialized)
                                    continue;
                                _simEnv.TraceLogger.LogDebug("Sending Delete command for error entity :" + _errorCreatingEntitys[kvp.Key].Organism);

                                // call its internal cleanup handler
                                _errorCreatingEntitys[kvp.Key].UnBindSynapseIO();

                                SimulationEngine.GlobalInstancePort.Delete((VisualEntity)_errorCreatingEntitys[kvp.Key].BaseObject);
                                StatsLogger.Sleeper(CleanUpPause);
                            }

                            _simEnv.TraceLogger.LogDebug("Sent Delete command to Simulation Engine for all Error Entitys");
                        }
                    }
                    finally
                    {
                        Monitor.Exit(_errorCreatingEntitys);
                    }
                }
                else
                {
                    throw new MSRoboticsEnvService.SimulationException("Error acquiring lock for _errorCreatingEntitys");
                }
            }
        }

        private void ClearErrorsEntityList()
        {
            if (Monitor.TryEnter(_errorCreatingEntitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    _errorCreatingEntitys.Clear();
                }
                finally
                {
                    Monitor.Exit(_errorCreatingEntitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Error acquiring lock on _errorCreatingEntitys");
            }
        }

        private void ClearEntitysList()
        {
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    _entitys.Clear();
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Error acquiring lock on _entitys");
            }
        }

        private void DeleteAllEntitys()
        {
            // lock entitys
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    // remove all from environment
                    foreach (var kvp in new Dictionary<Guid, IPhysicalEnvironment>(_entitys))
                    {
                        _simEnv.TraceLogger.LogDebug(string.Format("Removing entity {0} and has been initilized {1}", kvp.Key, (((VisualEntity)_entitys[kvp.Key].BaseObject).HasBeenInitialized)));

                        // make sure it has been init
                        if (!((VisualEntity)_entitys[kvp.Key].BaseObject).HasBeenInitialized)
                            continue;

                        // call the delete function
                        DeleteEntity(kvp.Key);
                    }
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Error acquiring lock on _entitys");
            }
        }

        /// <summary>
        /// Check internal state of environment etc, make sure entity counts etc are all equal
        /// </summary>
        public void CheckEntityStates()
        {

            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    _simEnv.TraceLogger.LogDebug("Starting Sanity Check");

                    // had some count mismatchs so code has been broken down
                    // count the number of born into the environment organims
                    long bornAliveCount = _simEnv.HostEnv.Organisms.Alive.LongCount(organisms => organisms.Born);
                    long bornDeadCount = _simEnv.HostEnv.Organisms.Dead.LongCount(organisms => organisms.Born);

                    // calc total
                    long bornCount = bornDeadCount + bornAliveCount;

                    _simEnv.TraceLogger.LogDebug(string.Format("Organism born : {0} ", bornCount));
                    _simEnv.TraceLogger.LogDebug(string.Format("Organism in Sim : {0} ", _entitys.Count));
                    _simEnv.TraceLogger.LogDebug(string.Format("Sim Engine Count : {0} ", SimulationEntityCount));

                    //check and make sure the count of organisms in our lists match up to the number in the environment
                    if (bornCount != _entitys.Count)
                    {
                        _simEnv.TraceLogger.LogDebug("Sanity Check Count Mismatch");
                        _simEnv.TraceLogger.LogDebug(string.Format("Organism Alive born count : {0} ", bornAliveCount));
                        _simEnv.TraceLogger.LogDebug(string.Format("Organism Dead born count : {0} ", bornDeadCount));
                        _simEnv.TraceLogger.LogDebug(string.Format("Organism Alive count : {0} ", _simEnv.HostEnv.Organisms.Alive.Count));
                        _simEnv.TraceLogger.LogDebug(string.Format("Organism Dead count : {0} ", _simEnv.HostEnv.Organisms.Dead.Count));

                        // if more in hosting environment
                        if (_entitys.Count > bornCount)
                        {
                            // lets find the one registereted in the environment but not in ours
                            foreach (KeyValuePair<Guid, IPhysicalEnvironment> kvp in _entitys)
                            {
                                IOrganism organism = _simEnv.HostEnv.Organisms.Find(kvp.Key);
                                // if we cant find it, it isnt born, or it had been terminated and is in the inactive list
                                if ((organism == null) || (!organism.Born) || (!(_simEnv.HostEnv.Organisms.Dead.Contains(organism) || _simEnv.HostEnv.Organisms.Alive.Contains(organism))))
                                {
                                    _simEnv.TraceLogger.LogDebug(string.Format("Organism in Alive : {0} ", _simEnv.HostEnv.Organisms.Alive.Contains(kvp.Value.Organism)));
                                    _simEnv.TraceLogger.LogDebug(string.Format("Organism in Dead : {0} ", _simEnv.HostEnv.Organisms.Dead.Contains(kvp.Value.Organism)));
                                    _simEnv.TraceLogger.LogDebug(string.Format("Organism in Inactive : {0} ", _simEnv.HostEnv.Organisms.Inactive.Contains(kvp.Value.Organism)));
                                    _simEnv.TraceLogger.LogDebug(string.Format("Organism {0} does not exist in main collection but exists in Sim Environment ", kvp.Value.Organism.Guid));
                                    var error = "Sanity Check : this organism does not exisit in our collection" + kvp.Value.Organism.Guid;
                                    _simEnv.TraceLogger.LogWarn(error);
                                    _simEnv.TriggerScenarioRestart(error);
                                    return;
                                }
                            }
                        }

                        // if more in born count
                        if (bornCount > _entitys.Count)
                        {
                            // todo : implement this

                            throw new MSRoboticsEnvService.SimulationException("Sanity Check method for (_bornCount > _entitys.Count) not implmented ");
                        }
#if DEBUG
                        // if we get here and still havent found the error dump  all the lists
                        _simEnv.TraceLogger.LogDebug("Main engine collection");
                        foreach (IOrganism organism in _simEnv.HostEnv.Organisms)
                        {
                            if (organism.Born)
                                _simEnv.TraceLogger.LogDebug(organism.Guid.ToString());
                        }
                        _simEnv.TraceLogger.LogDebug("Sim Env collection");
                        foreach (KeyValuePair<Guid, IPhysicalEnvironment> kvp in _entitys)
                            _simEnv.TraceLogger.LogDebug(kvp.Key.ToString());
#endif
                        throw new MSRoboticsEnvService.SimulationException("Sanity Check Failed, Hosting environment organisms count is out of sync");
                    }
                    _simEnv.TraceLogger.LogDebug("Starting Sanity Finished");
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }

            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire entitys lock");
            }
        }

        private void SaveEntity(IPhysicalEnvironment visualEntity, IOrganism organism)
        {
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    _entitys.Add(organism.Guid, visualEntity);
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire lock on _entitys");
            }
        }

        /// <summary>
        /// delete the entity from the simulation enviroment
        /// </summary>
        /// <param name="guid"></param>
        public void DeleteEntity(Guid guid)
        {
            _simEnv.TraceLogger.LogDebug(string.Format("Start - Deleting Organism {0}", guid));

            var entity = (VisualEntity)_entitys[guid].BaseObject;

            entity.SuspendUpdates = true;

            IPhysicalEnvironment origEntity = null;
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    origEntity = _entitys[guid];
                    _entitys.Remove(guid);
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire lock to remove cell");
            }

            if (origEntity != null)
            {
                origEntity.Dispose();
            }


            var deleteResetEvent = new ManualResetEvent(false);
            _simEnv.TraceLogger.LogDebug(string.Format("DeleteEntity ManualResetEvent Start with handle {0}", deleteResetEvent.GetHashCode()));

            _simDeleteEntity(entity, deleteResetEvent);

            if (!deleteResetEvent.WaitOne(_deleteEnitytWaitTimeout))
            {
                _simEnv.TraceLogger.LogDebug(string.Format("DeleteEntity ManualResetEvent Timed out with handle {0}", deleteResetEvent.GetHashCode()));
                const string errorMsg = "Error waiting on DELETE sync";
                _simEnv.TraceLogger.LogWarn(errorMsg);
                _simEnv.TriggerScenarioRestart(errorMsg);
            }

            deleteResetEvent.Close();
            _simEnv.TraceLogger.LogDebug(string.Format("DeleteEntity ManualResetEvent Close with handle {0}", deleteResetEvent.GetHashCode()));

            _simEnv.TraceLogger.LogDebug(string.Format("Finish - Deleting Organism {0}", guid));
        }

        public bool ContainsEntity(Guid entityGuid)
        {
            // lock the entitys to get a accurate count
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    return _entitys.ContainsKey(entityGuid);
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire lock on _entitys");
            }
        }

        public IPhysicalEnvironment GetWithGuidKey(Guid entityGuid)
        {
            // lock the entitys to get a accurate count
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    IPhysicalEnvironment entity;
                    _entitys.TryGetValue(entityGuid, out entity);
                    return entity;
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire lock on _entitys");
            }
        }

        /// <summary>
        /// Calculate the number of entitys consumed in the environment
        /// </summary>
        /// <returns></returns>
        public int CalculateEntitysConsumed()
        {
            _simEnv.TraceLogger.LogDebug(string.Format("Start CalculateEntitysConsumed, Entitys count : {0}", _entitys.Count));

            // lock the entitys to get a accurate count
            if (Monitor.TryEnter(_entitys, Globals.CriticalAcquireLockTimeout))
            {
                try
                {
                    return _entitys.Count;
                }
                finally
                {
                    Monitor.Exit(_entitys);
                }
            }
            else
            {
                throw new MSRoboticsEnvService.SimulationException("Unable to acquire lock on _entitys");
            }
        }

        public void AddBadEntity(Guid guid, IPhysicalEnvironment physicalEntity)
        {
            _errorCreatingEntitys.Add(guid, physicalEntity);
        }
    }
}